
AjaxRequests = {
  
  requests: $H({}),
  
  create: function(source, options) {
    if(this.requests[source]) {
      this.abortRequest(this.requests[source]);
      this.requests[source].setOptions(options);
      this.requests[source].request(source);
    }else {
      this.requests[source] = new Ajax.Request(source, options);
    }
  },

  abort: function() {
    this.requests.values().each(function(request) {
      this.abortRequest(request);
    }.bind(this));
  },
  
  clear: function() {
    this.requests = $H({});
  },

  abortRequest: function(request) {
    request.transport.abort();
    request.setOptions();
    request._complete = false;
  }
}

Spinner = Class.create();
Spinner.prototype = {
  initialize: function() {
  },

  run: function() {
    this.spinners().each(function(spinner) {
      spinner.src = spinner.src.gsub(/\.png$/, '-spinner.gif');
    });
  },

  stop: function() {
    this.spinners().each(function(spinner) {
      spinner.src = spinner.src.gsub(/-spinner\.gif$/, '.png');
    });
  },
  
  redraw: function() {
    this.spinners().each(function(spinner) {
      var tmp = spinner.src;
      spinner.src = '';
      spinner.src = tmp;
    });
  },
  
  spinners: function() {
    return Element.getElementsByClassName(document, 'spinner');
  }
}

ConfigurationMonitor = Class.create();
ConfigurationMonitor.prototype = Object.extend(new BuildObserver(), {
  DEFAULT_INTERVAL: 60, //sec
  MIN_INTERVAL: 1,
  
  initialize: function(source, interval, setupConfiguration) {
    this.source = $(source);
    this.interval = $(interval);
    this.spinner = new Spinner();
    this.setupConfiguration = setupConfiguration;
    this.configuration = new Configuration(this.source.value);
    this.configuration.addObserver(this);
  },

  reboot: function() {
    CC.Project.prototype.LATEST_BUILDS = $H({});
    this.restart();
  },
  
  startSetup: function() {
    this.status = this.running
    this.stop();
  },

  endSetup: function() {
    if(this.status) {
      this.restart();
    }
  },
  
  restart: function() {
    this.stop();
    this.startNewAutomator();
  },
  
  stop: function() {
    this.running = false;
    this.spinner.stop();
    if(this.executor) {
      this.executor.stop();
    }
    AjaxRequests.abort();
  },
  
  startNewAutomator: function() {
    this.running = true;
    this.spinner.run();
    if(this.source.value.blank()) {
      this.setupConfiguration();
      return;
    }
    this.configuration.source = this.source.value;
    this.executor = new ConfigurationGetter(this.configuration, this.timeout());
    this.executor.startDirectly();
  },

  timeout: function() {
    var t = parseInt(this.interval.value);
    if(t == NaN || t < this.MIN_INTERVAL) {
      t = this.DEFAULT_INTERVAL;
      this.interval.value = t
    }
    return t * 1000;
  },
  
  onEnd: function(monitors) {
    this.executor.startAfterTimeout();
  }
})

ConfigurationGetter = Class.create();
ConfigurationGetter.prototype = {
  initialize: function(configuration, timeout) {
    this.configuration = configuration;
    this.timeout = timeout;
    this.stopped = false;
  },
  
  startAfterTimeout: function() {
    if(this.stopped) {
      return;
    }
    
    this.timeoutId = window.setTimeout(this.startDirectly.bind(this), this.timeout);
  },
  
  startDirectly: function() {
    this.configuration.get();
  },
  
  stop: function() {
    this.stopped = true;
    if(this.timeoutId) {
      try {
        window.clearTimeout(this.timeoutId);
      }catch(e) {}
    }
  }
}

Configuration = Class.create();
Configuration.prototype = {
  initialize: function(source) {
    this.source = source;
    this.monitorObservers = [];
  },
  
  addObserver: function(observer) {
    this.monitorObservers.push(observer)
  },
  
  get: function() {
    AjaxRequests.abort();
    try {
      AjaxRequests.create(this.source, {
        method: 'get',
        requestHeaders: {"Cache-Control": "no-cache"},
        onSuccess: this.runMonitors.bind(this),
        onFailure: function() {alert("can't get configuration from source(" + this.source + ")!")}.bind(this)
      });
    }catch (e) {
      alert("can't get configuration from source(" + this.source + "): " + e);
    }
  },
  
  runMonitors: function(transport) {
    var remoteConfig = null;
    try {
      remoteConfig = transport.responseText.evalJSON(false);
    } catch (e) {
      alert("Can't parse configuration: " + e);
      return;
    }
    var users = $H(remoteConfig.users || {})
    var monitors = new Monitors();
    remoteConfig.projects.each(function(project) {
      project.users = Object.clone(users).merge($H(project.users || {}))
      monitors.push(new Monitor(project));
    });

    var notifier = new ObserverNotifier(this.monitorObservers.clone());
    notifier.addObservers(remoteConfig.observers)

    monitors.run(notifier);
  }
}

ObserverNotifier = Class.create();
ObserverNotifier.prototype = {
  initialize: function(observers) {
    this.observers = observers;
  },
  
  addObservers: function(observers) {
    if(observers) {
      observers.each(function(observer) {
        this.observers.push(observer)
      }.bind(this));
    }
  },
  
  notify: function(eventName, arg) {
    this.observers.each(function(observer) {
      observer.notify(eventName, arg);
    });
  }
}

ObserverNotifier.Null = Class.create();
ObserverNotifier.Null.prototype = {
  initialize: function() {
  },

  notify: function(observer) {
  }
}

Monitors = Class.create();
Monitors.prototype = {
  initialize: function() {
    this.monitors = [];
  },
  
  push: function(monitor) {
    this.monitors.push(monitor)
  },
  
  inspect: function() {
    return this.monitors.inspect();
  },
  
  run: function(observer) {
    observer.notify('onStart', this.monitors);
    if(this.monitors.length == 0) {
      observer.notify('onEnd', this.monitors);
      return;
    }

    this.monitors.each(function(monitor, index) {
      var isLastMonitor = index + 1 == this.monitors.length;
      monitor.report = function(build) {
        build.notify(observer);
        if(isLastMonitor) {
          observer.notify('onEnd', this.monitors);
        }
      }.bind(this);

      monitor.run(observer);
    }.bind(this));
  }
}

Monitor = Class.create();
Monitor.prototype = {
  initialize: function(configuration) {
    this.name = configuration.name;
    this.source = configuration.source;
    this.configuration = configuration;
    this.cc = new CC.Project(this);
  },
  
  run: function() {
    this.cc.lastBuild();
  },
  
  toString: function() {
    return this.name;
  },
  
  htmlId: function() {
    return 'build_' + this.name;
  }
}

RssReader = Class.create();
RssReader.prototype = {
  initialize: function(source, rssItemHandler) {
    this.source = source;
    this.rssItemHandler = rssItemHandler;
  },

  readRss: function() {
    AjaxRequests.create(this.source, {
      method: 'get',
      requestHeaders: {"Cache-Control": "no-cache"},
      onSuccess: this.parseRss.bind(this),
      onFailure: this.readRssFailed.bind(this)
    });
  },
  
  parseRss: function(transport) {
    var item = new RssReader.Item.Null();
    try {
      var items = transport.responseXML.getElementsByTagName('item')
      if(items && items.length > 0) {
        item = new RssReader.Item(items[0]);
      }
    }catch(e) {
      alert('parseRss: ' + e)
    }
    this.rssItemHandler(item)
  },
  
  readRssFailed: function() {
    this.rssItemHandler(null);
  }
}

RssReader.Item = Class.create();
RssReader.Item.prototype = {
  initialize: function(item) {
    this.myTitle = this.findChildData('title', item) || '';
    this.myPubTime = Date.parse(this.findChildData('pubDate', item).gsub(/ [zZ]$/, ''));
    this.myDescription = this.findChildData('description', item);
    this.myLink = this.findChildData('link', item);
  },
  
  title: function() {
    return this.myTitle;
  },
  
  pubTime: function() {
    return this.myPubTime
  },
  
  description: function() {
    return this.myDescription;
  },
  
  link: function() {
    return this.myLink;
  },
  
  findChildData: function(nodeName, item) {
    var child;
    for (child = item.firstChild; child != null; child = child.nextSibling) {
      if (child.nodeName == nodeName)
        return child.firstChild.data;
    }
    return null;
  },
  
  toBuild: function(cc) {
    return new CC.Build(this, cc.monitor)
  }
}

RssReader.Item.Null = Class.create();
RssReader.Item.Null.prototype = {
  initialize: function() {
  },
  
  toBuild: function(cc) {
    return new CC.Build.Null(cc.monitor);
  }
}

CC = Class.create();
CC.Project = Class.create();
CC.Project.prototype = {
  LATEST_BUILDS: $H({}),
  
  initialize: function(monitor) {
    this.rssReader = new RssReader(monitor.configuration.source, this.handleRssItem.bind(this));
    this.monitor = monitor;
  },
  
  lastBuild: function() {
    this.rssReader.readRss();
  },
  
  handleRssItem: function(rssItem) {
    var build = rssItem.toBuild(this);
    this.storeNewBuild(build);
    this.monitor.report(build);
  },
  
  storeNewBuild: function(build) {
    var latest = this.latestBuild();
    if(latest == undefined || latest.pubTime() < build.pubTime() || (latest.pubTime() != -1 && build.pubTime() == -1)) {
      if(latest) {
        latest.preNotifier = new ObserverNotifier.Null();
      }
      this.LATEST_BUILDS[build.name] = build
      build.isNew = true;
    }
    build.preNotifier = latest || new ObserverNotifier.Null();
  },
  
  latestBuild: function() {
    return this.LATEST_BUILDS[this.monitor.name]
  }
}

CC.Build = Class.create();
CC.Build.prototype = {
  
  initialize: function(rssItem, monitor) {
    this.isNew = false;
    this.rssItem = rssItem;
    this.name = monitor.name;
    this.monitorHtmlId = monitor.htmlId();
    this.users = monitor.configuration.users || $H({})
    if(monitor.configuration.build) {
      Object.extend(this, monitor.configuration.build)
    }
  },
  
  htmlId: function() {
    return this.monitorHtmlId
  },
  
  success: function() {
    try {
      return /(SUCCESS|FIXED|PASSED)/.test(this.rssItem.title().toUpperCase());
    }catch(e) {
      return false;
    }
  },
  
  notify: function(observer) {
    var eventName = 'onSkip'
    if(this.isNew) {
      eventName = this.success() ? 'onSuccess' : 'onFailure';
    }
    try {
      observer.notify(eventName, this);
    }catch(e) {
      alert('Observer notify ' + eventName + ' error: ' + e)
    }
  },
  
  link: function() {
    return this.rssItem.link();
  },
  
  pubTime: function() {
    return this.rssItem.pubTime();
  },
  
  message: function() {
    return this.name + ' build ' + (this.success() ? 'succeeded' : 'failed') + '. '
  },
  
  committers: function() {
    var committers = [];
    var pattern = /committed by (\w+)/;
    var source = this.rssItem.description();
    
    while (source.length > 0) {
      if (match = source.match(pattern)) {
        committer = this.users[match[1]] || match[1]
        if(!committers.include(committer)) {
          committers.push(committer)
        }
        source = source.slice(match.index + match[0].length);
      } else {
        source = '';
      }
    }

    if(committers.length == 0) {
      return ''
    }
    
    last = committers.pop()

    if(committers.length == 0) {
      return 'committed by ' + last + '. '
    }else {
      return 'committed by ' + committers.join(', ') + ' and ' + last + '. '
    }
  }
}

CC.Build.Null = Class.create();
CC.Build.Null.prototype = {
  isNull: true,
  
  initialize: function(monitor) {
    this.isNew = false;
    this.sourceLink = monitor.configuration.source;
    this.name = monitor.name;
    this.monitorHtmlId = monitor.htmlId();
  },
  
  htmlId: function() {
    return this.monitorHtmlId;
  },
  
  success: function() {
    return false;
  },
  
  link: function() {
    return this.sourceLink
  },

  pubTime: function() {
    return -1;
  },
  
  message: function() {
    return 'Loaded ' + this.name + ' build error. '
  },
  
  notify: function(observer) {
    var eventName = this.isNew ? 'onError' : 'onSkip';
    observer.notify(eventName, this);
  },
  
  committers: function() {
    return "Don't know committers. "
  }
}
